/**
 * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2025)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { AppNode, NO_SCRIPT_RUN_ID } from "./AppNode.interface"
import { block, text } from "./test-utils"
import { TransientNode } from "./TransientNode"
import { GetNodeByDeltaPathVisitor } from "./visitors/GetNodeByDeltaPathVisitor"
import { SetNodeByDeltaPathVisitor } from "./visitors/SetNodeByDeltaPathVisitor"

// prettier-ignore
const BLOCK = block([
  text("1"),
  block([
    text("2"),
  ]),
])

function getIn(node: AppNode, path: number[]): AppNode | undefined {
  return GetNodeByDeltaPathVisitor.getNodeAtPath(node, path)
}

describe("BlockNode", () => {
  describe("BlockNode.setIn (via SetNodeByDeltaPathVisitor)", () => {
    it("handles shallow paths", () => {
      const newBlock = SetNodeByDeltaPathVisitor.setNodeAtPath(
        BLOCK,
        [0],
        text("new"),
        NO_SCRIPT_RUN_ID
      )
      expect(getIn(newBlock, [0])).toBeTextNode("new")

      // Check BLOCK..newBlock diff is as expected.
      expect(newBlock).not.toStrictEqual(BLOCK)
      expect(getIn(newBlock, [1])).toStrictEqual(getIn(BLOCK, [1]))
    })

    it("handles deep paths", () => {
      const newBlock = SetNodeByDeltaPathVisitor.setNodeAtPath(
        BLOCK,
        [1, 1],
        text("new"),
        NO_SCRIPT_RUN_ID
      )
      expect(getIn(newBlock, [1, 1])).toBeTextNode("new")

      // Check BLOCK..newBlock diff is as expected
      expect(newBlock).not.toStrictEqual(BLOCK)
      expect(getIn(newBlock, [0])).toStrictEqual(getIn(BLOCK, [0]))
      expect(getIn(newBlock, [1])).not.toStrictEqual(getIn(BLOCK, [1]))
      expect(getIn(newBlock, [1, 0])).toStrictEqual(getIn(BLOCK, [1, 0]))
      expect(getIn(newBlock, [1, 1])).not.toStrictEqual(getIn(BLOCK, [1, 1]))
    })

    it("throws an error for invalid paths", () => {
      expect(() =>
        SetNodeByDeltaPathVisitor.setNodeAtPath(
          BLOCK,
          [1, 2],
          text("new"),
          NO_SCRIPT_RUN_ID
        )
      ).toThrow("Bad delta path index 2 (should be between [0, 1])")
    })
  })

  describe("BlockNode.accept", () => {
    it("calls visitBlockNode on the visitor", () => {
      const node = block([text("child1"), text("child2")])
      const mockVisitor = {
        visitElementNode: vi.fn().mockReturnValue("element-result"),
        visitBlockNode: vi.fn().mockReturnValue("block-result"),
        visitTransientNode: vi.fn().mockReturnValue("transient-result"),
      }

      const result = node.accept(mockVisitor)

      expect(mockVisitor.visitBlockNode).toHaveBeenCalledWith(node)
      expect(mockVisitor.visitElementNode).not.toHaveBeenCalled()
      expect(result).toEqual("block-result")
    })

    it("allows visitor to return the same node", () => {
      const node = block([text("child")])
      const identityVisitor = {
        visitElementNode: vi.fn(),
        visitBlockNode: vi.fn().mockReturnValue(node),
        visitTransientNode: vi.fn(),
      }

      const result = node.accept(identityVisitor)

      expect(result).toBe(node)
    })

    it("can return a modified BlockNode through visitor", () => {
      const originalNode = block([text("child1"), text("child2")])
      const transformVisitor = {
        visitElementNode: vi.fn(),
        visitBlockNode: vi.fn().mockReturnValue(block([text("transformed")])),
        visitTransientNode: vi.fn(),
      }

      const result = originalNode.accept(transformVisitor)

      expect(result).not.toBe(originalNode)
      expect(result.children).toHaveLength(1)
      expect(
        GetNodeByDeltaPathVisitor.getNodeAtPath(result, [0])
      ).toBeTextNode("transformed")
    })
  })
})

describe("BlockNode.replaceTransientNodeWithSelf", () => {
  it("returns this when transient node scriptRunId differs", () => {
    const b = block([text("c")], "runA")
    const t = new TransientNode("runB", text("anchor"), [text("t")], 1)
    const result = b.replaceTransientNodeWithSelf(t)
    expect(result).toBe(b)
  })

  it("returns this when transient node is effectively empty (no anchor, no transients)", () => {
    const b = block([], "runA")
    const t = new TransientNode("runA", undefined, [], 1)
    const result = b.replaceTransientNodeWithSelf(t)
    expect(result).toBe(b)
  })

  it("returns TransientNode anchored to this block with filtered transients", () => {
    const runId = "cur"
    const b = block([], runId)
    const keep = text("keep", runId)
    const drop = text("drop", "old")
    const t = new TransientNode(
      runId,
      text("old-anchor", "old"),
      [keep, drop],
      99
    )

    const result = b.replaceTransientNodeWithSelf(t) as TransientNode
    expect(result).toBeInstanceOf(TransientNode)
    expect(result.anchor).toBe(b)
    expect(result.transientNodes).toEqual([keep])
    expect(result.scriptRunId).toBe(runId)
    expect(result.deltaMsgReceivedAt).toBe(99)
  })
})
